﻿using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Disposables;
using System.IO;
using System.Linq;
using Collab.Modules.Editing.Interfaces;
using Collab.Modules.Interfaces.Session;
using Collab.Services.Remote.Entities;
using System.Diagnostics;
using Collab.Services.Remote.Contracts;
using System.Concurrency;

namespace Collab.Modules.Session
{
    public abstract class CollaborationSession : ICollaborationSession
    {
        protected readonly CompositeDisposable disposable = new CompositeDisposable();
        protected readonly ConcurrentDictionary<string, IEditBuffer> editBuffers = new ConcurrentDictionary<string, IEditBuffer>();
        protected readonly DocumentTracker documentTracker;
        protected readonly SolutionBuilder solutionBuilder;
        protected readonly User user;
        protected readonly StateTracker stateTracker;
        protected ICommonRemoteEvents commonCallbacks;
        protected ICommonRemoteMessages commonMessages;
        protected readonly Queue<BufferUpdate> changeQueue = new Queue<BufferUpdate>();
        protected readonly Dictionary<Guid, IList<BufferUpdate>> queuedUpdates = new Dictionary<Guid, IList<BufferUpdate>>();
        protected readonly Dictionary<Guid, int> expectedUpdateIndex = new Dictionary<Guid, int>(); 
        protected readonly Tuple<byte, byte, byte>[] colors = new Tuple<byte, byte, byte>[]
        {
            new Tuple<byte, byte, byte>(0xff,0x0,0x0),
            new Tuple<byte, byte, byte>(0x0, 0xff, 0x0), 
            new Tuple<byte, byte, byte>(0x0, 0x0, 0xff),
            new Tuple<byte, byte, byte>(0x0, 0xff, 0xff),
            new Tuple<byte, byte, byte>(0xff, 0x00, 0xff),
            new Tuple<byte, byte, byte>(0xff, 0xff, 0x00),
        };
        protected int updateIndex = 0;

        public ObservableCollection<User> Users { get; private set; }
        protected SessionState sessionState;
        public SessionState SessionState
        {
            get
            {
                return this.sessionState;
            }
            set
            {
                lock (this.changeQueue)
                {

                    if (this.sessionState == value)
                        return;
                    if (value == Session.SessionState.Ready)
                    {
                        while (this.changeQueue.Count > 0)
                        {
                            ApplyUpdate(this.changeQueue.Dequeue());
                        }
                    }
                    this.sessionState = value;
                }
            }
        }



        public CollaborationSession(string name, DocumentTracker documentTracker, StateTracker stateTracker, SolutionBuilder solutionBuilder)
        {
            if (string.IsNullOrEmpty(name))
                throw new ArgumentException("name");
            if (documentTracker == null)
                throw new ArgumentException("documentTracker");
            if (stateTracker == null)
                throw new ArgumentException("stateTracker");
            if (solutionBuilder == null)
                throw new ArgumentException("solutionTracker");

            this.SessionState = Session.SessionState.Connecting;

            this.solutionBuilder = solutionBuilder;
            this.disposable.Add(solutionBuilder);
            this.documentTracker = documentTracker;
            this.disposable.Add(documentTracker);
            this.stateTracker = stateTracker;
            this.stateTracker.PropertyChanged += (sender, e) =>
            {
                if (e.PropertyName == "SessionState")
                    this.SessionState = this.stateTracker.SessionState;
            };

            this.Users = new ObservableCollection<User>();
            this.user = new User(name, null);
        }

        protected void SetupCommonServiceOperations(ICommonRemoteEvents commonCallbacks, ICommonRemoteMessages commonMessages)
        {
            this.commonCallbacks = commonCallbacks;
            this.commonMessages = commonMessages;

            this.disposable.Add(this.commonCallbacks.WhenChangeReceived.Where(change => change.Sender != this.user.Id).ObserveOnDispatcher().Subscribe(ProcessUpdate));

            this.disposable.Add(this.commonCallbacks.WhenUserStatusReceived.Subscribe(HandleUserStatus));

            SetupUserEvents();
            SetupFileOperations();

            Collab.Modules.ViewTracker.HighlightManager.SelectionChanged += (file, selection) =>
            {
                var status = new UserStatus(this.user.Id, file.Replace(this.solutionBuilder.SolutionPath + Path.DirectorySeparatorChar, ""));
                status.Selection = selection;
                this.commonMessages.PublishUserStatus(status);
            };

        }

        private void SetupFileOperations()
        {
            this.solutionBuilder.WhenFileAdded.Subscribe(file => this.commonMessages.PublishFileAdded(new AddedFileInformation { ProjectItem = file, UserId = user.Id }));

            this.disposable.Add(this.commonCallbacks.WhenFileRequestReceived.ObserveOnDispatcher().Subscribe(fileRequest =>
            {
                Debug.WriteLine("Requested " + fileRequest.FileName);
                Scheduler.NewThread.Schedule(() =>
                {
                    string filepath = Path.Combine(this.solutionBuilder.SolutionPath, fileRequest.FileName); 
                    int chunkSize = 32 * 1024;
                    using (FileStream stream = new FileStream(filepath, FileMode.Open))
                    {
                        int count = 0;
                        var fileChunk = new FileChunk { FileName = fileRequest.FileName, Target = fileRequest.Sender, Data = new byte[chunkSize], EOF = false };
                        while ((count = stream.Read(fileChunk.Data, 0, chunkSize)) > 0)
                        {
                            if (count < chunkSize)
                                fileChunk.Data = fileChunk.Data.Take(count).ToArray();

                            this.commonMessages.PublishFileChunk(fileChunk);

                            fileChunk.Data = new byte[chunkSize];
                        }
                    }
                    this.commonMessages.PublishFileChunk(new FileChunk { FileName = fileRequest.FileName, Target = fileRequest.Sender, Data = null, EOF = true });

                });
            }));
        }

        private void SetupUserEvents()
        {
            this.disposable.Add(this.commonCallbacks.WhenUserConnected.ObserveOnDispatcher().Subscribe(x => AddUser(x)));
            this.disposable.Add(this.commonCallbacks.WhenUserDisconnected.ObserveOnDispatcher().Subscribe(x => this.Users.Remove(x)));
        }



        protected void HandleUserStatus(UserStatus status)
        {
            if (this.SessionState == Session.SessionState.Ready)
            {
                var user = this.Users.FirstOrDefault(u => u.Id == status.Sender);
                if (user == null)
                    return;
                if (status.Selection != user.Status.Selection)
                {
                    if (!status.Selection.IsEmpty)
                        Collab.Modules.ViewTracker.HighlightManager.Highlight(Path.Combine(this.solutionBuilder.SolutionPath, user.Status.ActiveFile), status.Selection, user.Color);
                    else
                        Collab.Modules.ViewTracker.HighlightManager.RemoveHighlights(Path.Combine(this.solutionBuilder.SolutionPath, user.Status.ActiveFile), user.Color);
                }
                user.Status = status;
            }
        }

        protected void ProcessUpdate(BufferUpdate update)
        {
            lock (this.changeQueue)
            {
                if (this.SessionState == Session.SessionState.Ready)
                    ApplyUpdate(update);
                else
                    this.changeQueue.Enqueue(update);
            }
        }

        private void ApplyUpdate(BufferUpdate update)
        {
            if (this.editBuffers.ContainsKey(update.DocumentName))
                this.editBuffers[update.DocumentName].OnNext(update);
            //if (update.UpdateIndex == this.expectedUpdateIndex[update.Sender])
            //{
            //    if (this.editBuffers.ContainsKey(update.DocumentName))
            //        this.editBuffers[update.DocumentName].OnNext(update);
            //    this.expectedUpdateIndex[update.Sender]++;
            //    var next = this.queuedUpdates[update.Sender].OrderBy(u => u.UpdateIndex).FirstOrDefault();
            //    if (next != null)
            //    {
            //        this.queuedUpdates[update.Sender].Remove(next);
            //        ApplyUpdate(next);
            //    }
            //}
            //else
            //{
            //    Debug.WriteLine("CHANGE ARRIVED OUT OF ORDER!");
            //    this.queuedUpdates[update.Sender].Add(update);
            //}
        }


        public void GoToFile(string fileName)
        {
            this.documentTracker.ActivateWindow(fileName);
        }

        public virtual void Dispose()
        {
            foreach (var buffer in this.editBuffers.Values)
                buffer.Dispose();
            this.disposable.Dispose();

        }

        protected void AddUser(User user)
        {
            this.expectedUpdateIndex[user.Id] = 0;
            this.queuedUpdates[user.Id] = new List<BufferUpdate>();
            user.Color = this.colors[this.Users.Count % this.colors.Length];
            this.Users.Add(user);
        }


    }
}
